<style>
body {
  word-wrap: break-word;
  background: #f5f5f5 center top no-repeat;
  background-size: auto 680px;
}

pre {
  white-space: pre-wrap;
}

a {
  text-decoration: none;
  color: #06c;
}

a:hover {
  color: #f00;
}

.main {
  max-width: 700px;
  margin: 0 auto;
  padding-bottom: 80px
}

.mainBox {
  margin-top: 12px;
  padding: 12px;
  border-radius: 6px;
  background: #fff;
  --border: 1px solid #0b1;
  box-shadow: 2px 2px 3px #aaa;
}


.btns button {
  display: inline-block;
  cursor: pointer;
  border: none;
  border-radius: 3px;
  background: #0b1;
  color: #fff;
  padding: 0 15px;
  margin: 3px 20px 3px 0;
  line-height: 36px;
  height: 36px;
  overflow: hidden;
  vertical-align: middle;
}

.btns button:active {
  background: #0a1;
}

.pd {
  padding: 0 0 6px 0;
}

.lb {
  display: inline-block;
  vertical-align: middle;
  background: #00940e;
  color: #fff;
  font-size: 14px;
  padding: 2px 8px;
  border-radius: 99px;
}
</style>


<template>
  <div class="main">
    <slot name="top"></slot>

    <div class="mainBox">
      <div class="pd">
        类型---：{{ type }}
        <span style="margin:0 20px">
          比特率: <input type="text" v-model="bitRate" style="width:60px"> kbps
        </span>
        采样率: <input type="text" v-model="sampleRate" style="width:60px"> hz
      </div>

      <div class="btns">
        <div>
          <!-- <button @click="recOpen">打开录音,请求权限</button> -->
          <button @click="recClose">关闭录音,释放资源</button>
        </div>

        <button @click="recStart">录制</button>
        <button @click="recStop" style="margin-right:80px">停止</button>

        <span style="display: inline-block;">
          <button @click="recPause">暂停</button>
          <button @click="recResume">继续</button>
        </span>
        <span style="display: inline-block;">
          <button @click="recPlayLast">播放</button>
          <button @click="recUploadLast">上传</button>
          <button @click="recDownLast">本地下载</button>
        </span>
      </div>
    </div>

    <div class="mainBox">
      <div
        style="height:100px;width:300px;border:1px solid #ccc;box-sizing: border-box;display:inline-block;vertical-align:bottom"
        class="ctrlProcessWave"></div>
      <div style="height:40px;width:300px;display:inline-block;background:#999;position:relative;vertical-align:bottom">
        <div class="ctrlProcessX" style="height:40px;background:#0B1;position:absolute;"
          :style="{ width: powerLevel + '%' }"></div>
        <div class="ctrlProcessT" style="padding-left:50px; line-height:40px; position: relative;">{{
          durationTxt + "/" + powerLevel }}</div>
      </div>
    </div>

    <div class="mainBox">
      <!-- 放一个 <audio ></audio> 播放器，标签名字大写，阻止uniapp里面乱编译 -->
      <AUDIO ref="LogAudioPlayer" style="width:100%"></AUDIO>

      <div class="mainLog">
        <div v-for="obj in logs" :key="obj.idx">
          <div :style="{ color: obj.color == 1 ? 'red' : obj.color == 2 ? 'green' : obj.color }">
            <!-- <template v-once> 在v-for里存在的bug，参考：https://v2ex.com/t/625317 -->
            <span v-once>[{{ getTime() }}]</span><span v-html="obj.msg" />

            <template v-if="obj.res">
              {{ intp(obj.res.rec.set.bitRate, 3) }}kbps
              {{ intp(obj.res.rec.set.sampleRate, 5) }}hz
              编码{{ intp(obj.res.blob.size, 6) }}b
              [{{ obj.res.rec.set.type }}]_{{ obj.res.durationTxt }}ms

              <button @click="recdown(obj.idx)">下载</button>
              <button @click="recplay(obj.idx)">播放</button>

              <span v-html="obj.playMsg"></span>
              <span v-if="obj.down">
                <span style="color:red">{{ obj.down }}</span>

                没弹下载？试一下链接或复制文本<button @click="recdown64(obj.idx)">生成Base64文本</button>

                <textarea v-if="obj.down64Val" v-model="obj.down64Val"></textarea>
              </span>
            </template>
          </div>
        </div>
      </div>
    </div>

    <slot name="bottom"></slot>
  </div>
</template>












<script>
//加载必须要的core，demo简化起见采用的直接加载类库，实际使用时应当采用异步按需加载
import Recorder from 'recorder-core' //注意如果未引用Recorder变量，可能编译时会被优化删除（如vue3 tree-shaking），请改成 import 'recorder-core'，或随便调用一下 Recorder.a=1 保证强引用
//需要使用到的音频格式编码引擎的js文件统统加载进来，这些引擎文件会比较大
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
//可选的扩展
// import 'recorder-core/src/extensions/waveview'
import { postAction, sql2apiAction, getAction, uploadAction } from '@api/manage'

export default {
  data() {
    return {
      type: "mp3"
      , bitRate: 16
      , sampleRate: 16000

      , duration: 0
      , durationTxt: "0"
      , powerLevel: 0

      , logs: [],



      // 测试
      send_frameBuffer: new Uint8Array(0),
      send_logNumber: 0,
      testRec: null,
      testFrames: [],
      testDuration: 0,
      testUseMerge: true, // 是否合并成一个完整音频
      SendFrameSize: 0,

      mergeBlob: null,
      WaveViewBak: Recorder.WaveView,

      // 新增：5秒打印相关变量
      lastPrintTime: 0,
      printInterval: 5000, // 5秒
      pendingFrames: [], // 暂存的音频帧
    }
  }
  , created: function () {
    this.Rec = Recorder;
  }
  , methods: {
    recClose: function () {
      var rec = this.rec;
      this.rec = null;
      if (rec) {
        rec.close();
        this.reclog("已关闭");
      } else {
        this.reclog("未打开录音", 1);
      };
    }
    , recStart: function () {
      if (this.rec) {
        this.rec.stop();
      };


      var This = this;
      var rec = this.rec = Recorder({
        type: This.type
        , bitRate: +This.bitRate
        , sampleRate: +This.sampleRate
        , onProcess: function (buffers, powerLevel, duration, sampleRate) {
          This.duration = duration;
          This.durationTxt = This.formatMs(duration, 1);
          This.powerLevel = powerLevel;
        },
        takeoffEncodeChunk: function (chunkBytes) {
          This.RealTimeSendTry(chunkBytes, false);
        },
      });

      rec.open(function () {
        rec.start();
        var set = rec.set;
        This.reclog("录制中：" + set.type + " " + set.sampleRate + "hz " + set.bitRate + "kbps", 2);
      }, function (msg, isUserNotAllow) {
        This.reclog((isUserNotAllow ? "UserNotAllow，" : "") + "打开失败：" + msg);
      });

      this.testRec = rec;
      this.testFrames = [];
      this.testDuration = 0;
      this.RealTimeSendReset();
    }
    , recPause: function () {
      // 1
      if (this.rec && Recorder.IsOpen()) {
        this.rec.pause();
      } else {
        this.reclog("未打开录音", 1);
      };
    }
    , recResume: function () {
      if (this.rec && Recorder.IsOpen()) {
        this.rec.resume();
      } else {
        this.reclog("未打开录音", 1);
      };
    }
    , recStop: function () {
      if (!(this.rec && Recorder.IsOpen())) {
        This.reclog("未打开录音", 1);
        return;
      }

      var This = this;
      var rec = This.rec;
      var stopNext = function () {
        This.RealTimeSendTry(new Uint8Array(0), true);//最后一次发送
      };

      rec.stop(function (blob, duration) {
        This.testDuration = duration; //测试打日志用的
        stopNext();
        if (This.testUseMerge) {
          return This.reclog("录制已结束:", "2", {
            blob: This.mergeBlob
            , duration: duration
            , durationTxt: This.formatMs(duration)
            , rec: rec
          });
        }
        This.reclog("停止录制", 2);
      }, function (s) {
        stopNext();
        This.reclog("录音失败：" + s, 1);
      });
    }





    , recPlayLast: function () {
      if (!this.recLogLast) {
        this.reclog("请先录音，然后停止后再播放", 1);
        return;
      };
      this.recplay(this.recLogLast.idx);
    }
    , recUploadLast: function () {
      if (!this.recLogLast) {
        this.reclog("请先录音，然后停止后再上传", 1);
        return;
      };
      var This = this;
      var blob = this.recLogLast.res.blob;

      //本例子假设使用原始XMLHttpRequest请求方式，实际使用中自行调整为自己的请求方式
      //录音结束时拿到了blob文件对象，可以用FileReader读取出内容，或者用FormData上传
      var api = "http://127.0.0.1:9528";
      var onreadystatechange = function (xhr, title) {
        return function () {
          if (xhr.readyState == 4) {
            if (xhr.status == 200) {
              This.reclog(title + "上传成功" + ' <span style="color:#999">response: ' + xhr.responseText + '</span>', 2);
            } else {
              This.reclog(title + "没有完成上传，演示上传地址无需关注上传结果，只要浏览器控制台内Network面板内看到的请求数据结构是预期的就ok了。", "#d8c1a0");

              console.error(title + "上传失败", xhr.status, xhr.responseText);
            };
          };
        };
      };
      This.reclog("开始上传到" + api + "，请稍候... （你可以先到源码 /assets/node-localServer 目录内执行 npm run start 来运行本地测试服务器）");

      /***方式一：将blob文件转成base64纯文本编码，使用普通application/x-www-form-urlencoded表单上传***/
      //var reader = new FileReader();
      //reader.onloadend = function () {
      //  var postData = "";
      //  postData += "mime=" + encodeURIComponent(blob.type);//告诉后端，这个录音是什么格式的，可能前后端都固定的mp3可以不用写
      //  postData += "&upfile_b64=" + encodeURIComponent((/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1]) //录音文件内容，后端进行base64解码成二进制
      //  //...其他表单参数
//
      //  var xhr = new XMLHttpRequest();
      //  xhr.open("POST", api + "/uploadBase64");
      //  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
      //  xhr.onreadystatechange = onreadystatechange(xhr, "上传方式一【Base64】");
      //  xhr.send(postData);
      //};
      //reader.readAsDataURL(blob);

      /***方式二：使用FormData用multipart/form-data表单上传文件***/
      var form = new FormData();
      form.append("upfile", blob, "recorder.mp3"); //和普通form表单并无二致，后端接收到upfile参数的文件，文件名为recorder.mp3
      //...其他表单参数

      var xhr = new XMLHttpRequest();
      xhr.open("POST", api + "/upload");
      xhr.onreadystatechange = onreadystatechange(xhr, "上传方式二【FormData】");
      xhr.send(form);
    }
    , recDownLast: function () {
      if (!this.recLogLast) {
        this.reclog("请先录音，然后停止后再下载", 1);
        return;
      };
      this.recdown(this.recLogLast.idx);
    }










    , reclog: function (msg, color, res) {
      console.error('reclog', msg, color, res);
      var obj = {
        idx: this.logs.length
        , msg: msg
        , color: color
        , res: res

        , playMsg: ""
        , down: 0
        , down64Val: ""
      };
      if (res && res.blob) {
        this.recLogLast = obj;
      };
      this.logs.splice(0, 0, obj);
    }
    , recplay: function (idx) {
      var This = this;
      var o = this.logs[this.logs.length - idx - 1];
      o.play = (o.play || 0) + 1;
      var logmsg = function (msg) {
        o.playMsg = '<span style="color:green">' + o.play + '</span> ' + This.getTime() + " " + msg;
      };
      logmsg("");

      var audio = this.$refs.LogAudioPlayer;
      audio.controls = true;
      if (!(audio.ended || audio.paused)) {
        audio.pause();
      };
      audio.onerror = function (e) {
        logmsg('<span style="color:red">播放失败[' + audio.error.code + ']' + audio.error.message + '</span>');
      };
      audio.src = (window.URL || webkitURL).createObjectURL(o.res.blob);
      audio.play();
    }
    , recdown: function (idx) {
      var This = this;
      var o = this.logs[this.logs.length - idx - 1];
      o.down = (o.down || 0) + 1;

      o = o.res;
      var name = "rec-" + o.duration + "ms-" + (o.rec.set.bitRate || "-") + "kbps-" + (o.rec.set.sampleRate || "-") + "hz." + (o.rec.set.type || (/\w+$/.exec(o.blob.type) || [])[0] || "unknown");
      var downA = document.createElement("A");
      downA.href = (window.URL || webkitURL).createObjectURL(o.blob);
      downA.download = name;
      downA.click();
    }
    , recdown64: function (idx) {
      var This = this;
      var o = this.logs[this.logs.length - idx - 1];
      var reader = new FileReader();
      reader.onloadend = function () {
        o.down64Val = reader.result;
      };
      reader.readAsDataURL(o.res.blob);
    }
    , getTime: function () {
      var now = new Date();
      var t = ("0" + now.getHours()).substr(-2)
        + ":" + ("0" + now.getMinutes()).substr(-2)
        + ":" + ("0" + now.getSeconds()).substr(-2);
      return t;
    }
    , formatMs: function (ms, all) {
      var ss = ms % 1000; ms = (ms - ss) / 1000;
      var s = ms % 60; ms = (ms - s) / 60;
      var m = ms % 60; ms = (ms - m) / 60;
      var h = ms;
      var t = (h ? h + ":" : "")
        + (all || h + m ? ("0" + m).substr(-2) + ":" : "")
        + (all || h + m + s ? ("0" + s).substr(-2) + "″" : "")
        + ("00" + ss).substr(-3);
      return t;
    }
    , intp: function (s, len) {
      s = s == null ? "-" : s + "";
      if (s.length >= len) return s;
      return ("_______" + s).substr(-len);
    },




    RealTimeSendReset: function () {
      this.send_frameBuffer = new Uint8Array(0);
      this.send_logNumber = 0;
      this.lastPrintTime = Date.now();
      this.pendingFrames = [];
    },
    RealTimeSendTry: function (chunkBytes, isClose) {
      // 将数据添加到暂存队列
      if (chunkBytes.length > 0) {
        this.pendingFrames.push(new Uint8Array(chunkBytes));
      }

      // 获取当前时间戳
      const now = Date.now();
      const timeInterval = now - this.lastPrintTime;

      // 如果是关闭录音或者时间间隔达到5秒，则进行打印
      if (isClose || timeInterval >= this.printInterval) {
        console.error('进入合并数据帧', timeInterval)
        // 合并所有暂存的帧数据
        this.mergeAndPrintFrames();
        this.lastPrintTime = now;
      }

      // 关闭录音时的处理
      if (isClose) {
        var number = this.send_logNumber;
        console.log("[No." + (number < 100 ? ("000" + number).substr(-3) : number) + "]已停止传输");
        this.testMergeFrames();
      }
    },

    // 新增：合并并打印暂存的帧数据
    mergeAndPrintFrames: function () {
      if (this.pendingFrames.length === 0) return;

      // 计算所有帧的总大小
      let totalSize = 0;
      for (let i = 0; i < this.pendingFrames.length; i++) {
        totalSize += this.pendingFrames[i].length;
      }

      // 合并所有帧
      const mergedBytes = new Uint8Array(totalSize);
      let offset = 0;
      for (let i = 0; i < this.pendingFrames.length; i++) {
        mergedBytes.set(this.pendingFrames[i], offset);
        offset += this.pendingFrames[i].length;

        // 如果需要保存到测试帧中用于后续合并
        if (this.testUseMerge) {
          this.testFrames.push(this.pendingFrames[i]);
        }
      }

      // 打印合并后的数据
      const number = ++this.send_logNumber;
      this.testLogFrame(mergedBytes, number);

      // 清空暂存队列
      this.pendingFrames = [];
    },

    testMergeFrames: function () {
      if (!this.testUseMerge) {
        return console.log("未勾选合并成一个完整音频，不测试合并成完整音频文件", "#aaa");
      }
      var size = 0;
      for (var i = 0; i < this.testFrames.length; i++) size += this.testFrames[i].length;
      if (size == 0) {
        return console.log("未录制得到音频数据帧，不测试合并成完整音频文件", 1);
      }
      var bytes = new Uint8Array(size);
      for (var i = 0, offset = 0; i < this.testFrames.length; i++) {
        bytes.set(this.testFrames[i], offset);
        offset += this.testFrames[i].length;
      }
      var recSet = this.rec.set;
      var blob = new Blob([bytes.buffer], {
        type: "audio/" + recSet.type
      });
      this.mergeBlob = blob
      console.log("合并成一个完整音频", this.mergeBlob);
      console.error(blob, this.testDuration, this.rec, this.testFrames.length + "帧数据合并成一个完整音频");
    },
    testLogFrame: function (frameBytes, number) {
      var recSet = this.rec.set;
      var dur = 0; //简单估算数据时长，不一定准确
      dur = Math.round(frameBytes.length / recSet.bitRate * 8);

      var blob = new Blob([frameBytes.buffer], { type: "audio/" + recSet.type });
      var logMsg = "No." + (number < 100 ? ("000" + number).substr(-3) : number);
      console.log('logMsg--', logMsg);
      this.reclog(`小CEO录制中__${logMsg}__间隔${this.printInterval}ms`, "", {
        blob: blob
        , duration: dur
        , durationTxt: this.formatMs(dur)
        , rec: this.rec
      });

       var form = new FormData();
        form.append("file", blob, "recorder.mp3"); //和普通form表单并无二致，后端接收到upfile参数的文件，文件名为recorder.mp3
        form.append("grId", localStorage.getItem('$gridId'));
        form.append("isFinish", "false");


     uploadAction('/common/sound/recording/uploadLiveFile', form, 300000).then((response) => {


     })
      if (true && number % 100 == 0) {
        // Runtime.LogClear();
      };
    }
  }
}
</script>